package world;

import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.Scanner;

import parsing.CritterParser;
import parsing.InvalidSyntaxException;
import parsing.productions.AbstractNode;
import parsing.productions.Program;
import parsing.productions.Rule;

public class Critter {

	private CritterWorld world;
	private Hex hex;
	private Program genome;
	/*
	 * mem[0] is memory length (immutable).
	 * mem[1] is defense (immutable).
	 * mem[2] is offense (immutable).
	 * mem[3] is size (cannot be assigned).
	 * mem[4] is energy (cannot be assigned).
	 * mem[5] is appearance. 1 is not mating, 2 is mating.
	 * mem[6] is rule pass counter (cannot be assigned).
	 */
	private int[] mem;
	private int dir; // this Critter's direction, 0..5, starting facing north and going clockwise.
	private boolean hasActed; // has acted during this timestep.
	private int lastDamage; // the damage dealt by this Critter during the last timestep
	private boolean justAttacked; // attacked during the last timestep
	private Rule lastRule; //the Rule this Critter executed at the end of its last turn
	
	// Critter command constants - used when controlling a Critter manually
	public static final int REST		= 0;
	public static final int FORWARD		= 1;
	public static final int BACKWARD	= 2;
	public static final int LEFT 		= 3;
	public static final int RIGHT		= 4;
	public static final int EAT			= 5;
	public static final int ATTACK		= 6;
	public static final int GROW		= 7;
	public static final int BUD 		= 8;
	public static final int MATE 		= 9;
	
	/**
	 * Returns the value of this Critter's memory at the specified index.
	 * @throws IllegalArgumentException if index is not a valid index in this Critter's mem array.
	 */
	public int getMemValue(int index) {
		if (index >= mem.length || index < 0)
			throw new IllegalArgumentException("This Critter does not have a memory value at the specified index.");
		return mem[index];
	}
	
	/**
	 * Sets the value of this Critter's mem array at the specified index to the given value.
	 * @throws IllegalArgumentException if index is not a valid index in this Critter's mem array
	 * or if the index is of a mem value that is not assignable.
	 */
	public void setMemValue(int index, int newVal){
		if (index >= mem.length || index < 0)
			throw new IllegalArgumentException("This Critter does not have a memory value at the specified index.");
		if (!(index == 5 || index > 6))
			throw new IllegalArgumentException("memory value " + index + " cannot be directly assigned");
		mem[index] = newVal;
	}
	
	/**
	 * Returns the Program that this Critter obeys
	 */
	public Program getGenome(){
		return genome;
	}
	
	public void setGenome(Program p){
		genome = p;
	}
	
	/**
	 * Returns the Rule this Critter executed at the end of its last turn
	 * Null if this Critter did not act according to its genome
	 */
	public Rule getLastRule(){
		return lastRule;
	}
	
	public void setLastRule(Rule r){
		lastRule = r;
	}
	
	public void incrementRuleCount(){
		mem[6]++;
	}
	
	public void resetRuleCount(){
		mem[6] = 0;
	}
	
	public int getDirection(){
		return dir;
	}
	
	/**
	 * Returns the damage dealt by this Critter on the last timestep.
	 * 0 if it did not attack.
	 */
	public int getLastDamage(){
		return lastDamage;
	}
	
	public boolean hasActed(){
		return hasActed;
	}
	
	/**
	 * Creates a new Critter
	 * Checks: given Hex is not occupied and memLength is >= 7.
	 * @param w the CritterWorld this Critter is in
	 * @param h the Hex that contains this Critter
	 * @param memLength
	 * @param defense
	 * @param offense
	 * @throws IllegalArgumentException if the given Hex is occupied (by a Critter or Rock)
	 */
	public Critter(CritterWorld w, Hex h, int memLength, int defense, int offense){
		if(h.isOccupied())
			throw new IllegalArgumentException("Critter creation error: cannot create a Critter in an occupied Hex.");
		if(memLength < 7)
			throw new IllegalArgumentException("Critter creation error: cannot create a Critter with memLength less than seven.");
		world = w;
		hex = h;
		hex.putCritter(this);
		mem = new int[memLength];
		mem[0] = memLength;
		mem[1] = defense;
		mem[2] = offense;
		mem[3] = 1;
		mem[4] = world.INITIAL_ENERGY;
		mem[5] = 1;
		mem[6] = 0;
		dir = (int)(Math.random()*6);//between 0 and 5
		world.putCritter(this);
	}
	
	/**
	 * Creates a new Critter controlled by a Program
	 * Checks: given Hex is not occupied and memLength is >= 7.
	 * @param w the CritterWorld this Critter is in
	 * @param h the Hex that contains this Critter
	 * @param p the Program that will control this Critter
	 * @param memLength
	 * @param defense
	 * @param offense
	 * @throws IllegalArgumentException if the given Hex is occupied (by a Critter or Rock)
	 */
	public Critter(CritterWorld w, Hex h, Program p, int memLength, int defense, int offense){
		this(w, h, memLength, defense, offense);
		genome = p;
		mem[6] = 0;
	}
	
	/**
	 * Creates a new Critter by loading data from the given file name.
	 * @param w this Critter's CritterWorld
	 * @param h this Critter's Hex
	 * @param direction The direciton this Critter is facing
	 * @param genomeFile The name of the file to load data from.
	 * @throws IOException if genomeFile was not found or is not formatted properly
	 * @throws IllegalArgumentException if this critter is on an occupied Hex or its memSize is < 7
	 * @throws InvalidSyntaxException if the genome found in genomeFile is incorrectly formatted
	 */
	public Critter(CritterWorld w, Hex h, int direction, String genomeFile) throws IOException, IllegalArgumentException, InvalidSyntaxException {
		Scanner genomeScanner = new Scanner(new FileReader(genomeFile));
		int memSize = 0, defense = 0, offense = 0, size = 0, energy = 0, appearance = 0;
		try {
			int values = 0;
			String lineStart;
			while(values < 6){
				lineStart = genomeScanner.next();
				if("memsize:".equalsIgnoreCase(lineStart)){
					memSize = Integer.parseInt(genomeScanner.next());
					values++;
				}
				else if("defense:".equalsIgnoreCase(lineStart)){
					defense = Integer.parseInt(genomeScanner.next());
					values++;
				}
				else if("offense:".equalsIgnoreCase(lineStart)){
					offense = Integer.parseInt(genomeScanner.next());
					values++;
				}
				else if("size:".equalsIgnoreCase(lineStart)){
					size = Integer.parseInt(genomeScanner.next());
					values++;
				}
				else if("energy:".equalsIgnoreCase(lineStart)){
					energy = Integer.parseInt(genomeScanner.next());
					values++;
				}
				else if("appearance:".equalsIgnoreCase(lineStart)){
					appearance = Integer.parseInt(genomeScanner.next());
					values++;
				}
				if(values < 6)
					genomeScanner.nextLine();
			}
		} catch (NumberFormatException e) {
			throw new IOException("Criter file:\n" + genomeFile + "\nwas not formatted properly.");
		} catch (NoSuchElementException e){
			throw new IOException("Criter file:\n" + genomeFile + "\ndid not specify all critter atributes.");
		}
		CritterParser cp = new CritterParser();
		genomeScanner.nextLine();
		StringBuffer sb = new StringBuffer();
		while(genomeScanner.hasNextLine())
			sb.append(genomeScanner.nextLine() + "\n");
		genome = cp.parse(new StringReader(sb.toString()));
		if(h.isOccupied())
			throw new IllegalArgumentException("Critter creation error: cannot create a Critter in an occupied Hex.");
		if(memSize < 7)
			throw new IllegalArgumentException("Critter creation error: cannot create a Critter with memLength less than seven." +
											 "\nFile: "+ genomeFile);
		world = w;
		hex = h;
		hex.putCritter(this);
		mem = new int[memSize];
		mem[0] = memSize;
		mem[1] = defense;
		mem[2] = offense;
		mem[3] = size;
		mem[4] = energy;
		mem[5] = appearance;
		mem[6] = 0;
		dir = direction % 6;
		world.putCritter(this);
	}
	
	/**
	 * Returns the Hex this Critter is in
	 */
	public Hex getHex(){
		return hex;
	}
	
	/**
	 * Returns the World this Critter is in
	 */
	public CritterWorld getWorld(){
		return world;
	}
	
	/**
	 * Moves this Critter onto the Hex in the direction given. Uses energy.
	 * @return true if move was successful.
	 */
	private boolean move(int direction) {
		if (useEnergy(world.MOVE_COST * mem[3])) return false;
		Location newLoc = hex.getLocation().nextLocation(direction);
		if (!newLoc.isValid())
			return false;
		Hex inFront = world.getHex(newLoc);
		if (inFront.isOccupied())
			return false;
		hex.removeCritter();
		hex = inFront;
		hex.putCritter(this);
		return true;
	}

	/**
	 * Moves this Critter onto the Hex behind it if possible. Uses energy.
	 * @return true if move was successful.
	 */
	public boolean backward() {
		return move((dir + 3)%6);
	}
	
	/**
	 * Moves this Critter onto the Hex in front of it if possible. Uses energy.
	 * @return true if move was successful.
	 */
	public boolean forward() {
		return move(dir);
	}
	
	/**
	 * This Critter does nothing except for use energy.
	 */
	public void rest() {
		useEnergy(mem[3]);
	}
	
	/**
	 * This Critter turns counterclockwise. Uses energy.
	 */
	public void left() {
		useEnergy(mem[3]);
		dir = (dir + 5) % 6;
	}
	
	/**
	 * This Critter turns clockwise. Uses energy.
	 */
	public void right() {
		useEnergy(mem[3]);
		dir = (dir + 1) % 6;
	}
	
	/**
	 * This Critter eats all the food and plant (if any) on its Hex. Uses energy.
	 */
	public void eat() {
		if (!useEnergy(mem[3])) {
			mem[4] += (hex.foodEaten() + hex.plantEaten());
			if (mem[4] > mem[3] * world.ENERGY_PER_SIZE)
				mem[4] = mem[3] * world.ENERGY_PER_SIZE;
		}
	}
	
	/**
	 * This Critter attacks the Critter in front of it if there is one. Uses energy.
	 * @return true if a Critter was attacked.
	 */
	public boolean attack() {
		if (useEnergy(world.ATTACK_COST * mem[3]))
			return false;
		Location inFront = hex.getLocation().nextLocation(dir);
		if (!inFront.isValid())
			return false;
		Hex forwardHex = world.getHex(inFront);
		if (!forwardHex.hasCritter())
			return false;
		Critter attacked = forwardHex.getCritter();
		double logParam = world.DAMAGE_INC * (mem[3]*mem[2] - attacked.mem[3]*attacked.mem[1]);
		double logistic = 1 / (1 + Math.pow(Math.E, (-1 * logParam)));
		int damage = (int)(world.BASE_DAMAGE * mem[3] * logistic);
		attacked.injure(damage);
		lastDamage = damage;
		justAttacked = true;
		return true;
	}
	
	/**
	 * This Critter increases in size if possible. Uses energy.
	 */
	public void grow() {
		if (!useEnergy(world.GROW_COST * mem[3] * complexity())) {
			mem[3]++;
		}
	}
	
	/**
	 * If possible, this Critter buds and the new Critter is placed behind it.
	 * @return true if this Critter buds successfully.
	 */
	public boolean bud() {
		if (useEnergy(world.BUD_COST * complexity()))
			return false;
		Location baby = hex.getLocation().nextLocation((dir + 3)%6);
		if (!baby.isValid())
			return false;
		Hex babyCrib = world.getHex(baby);
		if (babyCrib.isOccupied())
			return false;
		if(genome != null){
			double mutating = Math.random();
			Program newProgram = (Program) genome.duplicate();
			while (mutating < 0.5) {
				Program.mutate(newProgram);
				mutating = Math.random();
			}
			babyCrib.putCritter(new Critter(world, babyCrib, newProgram, mem[0], mem[1], mem[2]));
		} else
			babyCrib.putCritter(new Critter(world, babyCrib, mem[0], mem[1], mem[2]));			
		return true;
	}
	
	/**
	 * Tries to mate with a Critter in front of it. Uses energy.
	 * Can only mate if the other Critter is facing it and trying to mate.
	 * If the other Critter has not acted yet, this Critter does not make a baby,
	 * but this Critter changes its mating status to true.
	 * @return true if this Critter makes a baby.
	 */
	public boolean mate() {
		Location inFront = hex.getLocation().nextLocation(dir);
		if (!inFront.isValid())
			return false;
		Hex partnerHex = world.getHex(inFront);
		if (partnerHex.isRock() || !partnerHex.hasCritter())
			return false;
		Critter partner = partnerHex.getCritter();
		if (partner.dir != (dir + 3)%6)
			return false;
		if (!partner.hasActed) {
			mem[5] = 2;
			hasActed = true;
			return false;
		}
		if (partner.mem[5] != 2)
			return false;
		Random rnd = new Random();
		int randomHex = rnd.nextInt(2);
		if (randomHex == 0)
			return makeBaby(partner);
		else
			return partner.makeBaby(this);
	}
	
	/**
	 * Calculates the complexity of this Critter.
	 * @return the complexity.
	 */
	private int complexity() {
		return ((genome == null) ? 0 : genome.getChildren().size()) *
				world.RULE_COST + (mem[1] + mem[2]) * world.ABILITY_COST;
	}
	
	/**
	 * Removes given amount of energy from this Critter and checks if the Critter dies.
	 * Marks this Critter as having acted.
	 * @param eUsed the energy to be taken away.
	 * @return true if this Critter dies.
	 */
	private boolean useEnergy(int eUsed) {
		mem[4] -= eUsed;
		hasActed = true;
		if (mem[4] <= 0) {
			hex.removeCritter();
			hex.putFood(world.FOOD_PER_SIZE * mem[3]);
			world.removeCritter(this);
			return true;
		}
		return false;
	}
	
	/**
	 * Removes given amount of energy from this Critter and checks if the Critter dies.
	 * Does not mark this Critter as having acted.
	 * @param eUsed the energy to be taken away.
	 * @return true if this Critter dies.
	 */
	private boolean injure(int damage) {
		mem[4] -= damage;
		if (mem[4] <= 0) {
			hex.removeCritter();
			hex.putFood(world.FOOD_PER_SIZE * mem[3]);
			world.removeCritter(this);
			return true;
		}
		return false;
	}
	
	/**
	 * Ensures that this Critter is ready for the next timestep of the simulation.
	 * @return true if the Critter is alive for the next turn
	 */
	public boolean getReady() {
		hasActed = false;
		if(mem[5] == 2){
			mem[5] = 1;
			return !useEnergy(mem[3]);
		}
		if(justAttacked)
			justAttacked = false;
		else
			lastDamage = 0;
		return true;
	}
	
	/**
	 * If possible, makes a baby Critter and puts it behind this Critter.
	 * @param partner the mating partner of this Critter.
	 * @return true if a baby was made.
	 */
	private boolean makeBaby(Critter partner) {
		// Determine location and decide if mating is successful.
		mem[5] = 1;
		partner.mem[5] = 1;
		if (useEnergy(world.MATE_COST * complexity()))
			return false;
		if (partner.useEnergy(world.MATE_COST * partner.complexity()))
			return false;
		Location loc = hex.getLocation().nextLocation((dir + 3)%6);
		if (!loc.isValid())
			return false;
		Hex babyCrib = world.getHex(loc);
		if (babyCrib.isOccupied())
			return false;
		Random rnd = new Random();
		int[] earlyAttr = new int[3];
		for (int i = 0; i < 3; i++) {
			int parentChosen = rnd.nextInt(2);
			if (parentChosen == 0)
				earlyAttr[i] = mem[i];
			else
				earlyAttr[i] = partner.mem[i];
		}
		if(genome != null && partner.genome != null){
			// Mix genomes.
			int parentGenome = rnd.nextInt(2);
			int numParentRules = genome.getChildren().size();
			int numPartnerRules = partner.genome.getChildren().size();
			List<AbstractNode> longerRules;
			List<AbstractNode> shorterRules;
			if(numParentRules < numPartnerRules){
				longerRules = partner.genome.getChildren();
				shorterRules = genome.getChildren();
			}
			else{
				longerRules = genome.getChildren();
				shorterRules = partner.genome.getChildren();
			}
			int numRules;
			if (parentGenome == 0)
				numRules = numParentRules;
			else
				numRules = numPartnerRules;
			List<AbstractNode> rules = new LinkedList<AbstractNode>();
			for (int i = 0; i < numRules; i++) {
				int parent = rnd.nextInt(2);
				if (parent == 0 && i < shorterRules.size()) {
					rules.add((AbstractNode) shorterRules.get(i).duplicate());
				} else{//parent == 1 || i >= shorterRules.size()
					rules.add((AbstractNode) longerRules.get(i).duplicate());
				}
			}
			Program newProgram = new Program(rules);
			// Mutate if necessary.
			double mutating = Math.random();
			while (mutating < 0.5) {
				Program.mutate(newProgram);
				mutating = Math.random();
			}
			babyCrib.putCritter(new Critter(world, babyCrib, newProgram, earlyAttr[0], earlyAttr[1], earlyAttr[2]));
		} else
			babyCrib.putCritter(new Critter(world, babyCrib, earlyAttr[0], earlyAttr[1], earlyAttr[2]));
		return true;
	}
	
	/**
	 * This Critter takes a randomly chosen action.
	 */
	public void takeRandomAction() {
		Random rnd = new Random();
		int action = rnd.nextInt(10);
		takeSpecifiedAction(action);
	}
	
	/**
	 * This Critter acts according to its genome
	 * If there is no genome, it takes a random action
	 */
	public void act(){
		if(genome == null){
			takeRandomAction();
		}
		else
			genome.execute(this);
	}

	/**
	 * This Critter takes the specified Action, if Action is not one of the values
	 * defined in the constants in Critter, this Critter will rest.
	 * @param action the value of the action, defined by action constants in Critter.
	 */
	public void takeSpecifiedAction(int action) {
		switch(action) {
		case 1: forward(); break;
		case 2: backward(); break;
		case 3: left(); break;
		case 4: right(); break;
		case 5: eat(); break;
		case 6: attack(); break;
		case 7: grow(); break;
		case 8: bud(); break;
		case 9: mate(); break;
		default: rest(); break;
		}
	}

}
